package com.hmdp.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.BooleanUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.*;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBloomFilter;
import org.springframework.data.geo.*;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author Cong
 * @since 2021-12-22
 */
@Service
@Slf4j
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
    private static RBloomFilter<Long> shopIdFilter;

    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    @Resource
    private CacheClient cacheClient;
    @Resource(name = "stringRedisTemplate")
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private BloomFilterUtil bloomFilterUtil;


    @PostConstruct // 项目启动的时候执行该方法
    public void init() {
        // 启动项目时初始化bloomFilter
        List<Shop> shopList = list();//查询所有店铺
        redisTemplate.delete("ShopIdFilter");
        shopIdFilter = bloomFilterUtil.create("ShopIdFilter", RedisConstants.BLOOM_FILTER_EXPECTED_INSERT_NUM, RedisConstants.BLOOM_FILTER_FPP);
        for (Shop shop : shopList) {
            shopIdFilter.add(shop.getId());//将店铺id加入布隆过滤器
        }
    }

    /**
     * 根据id查询店铺信息，优先查redis，后查数据库，数据库查到后添加到redis
     * @param id 店铺id
     * @return
     */
    @Override
    public Result queryById(Long id) {
        if (!shopIdFilter.contains(id)) {
            return Result.fail("BloomFilter---店铺不存在！");
        }
        //缓存穿透
        Shop shop = cacheClient.queryWithPassThrough(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, shopId -> getById(shopId), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);

        //用互斥锁解决缓存击穿
        //Shop shop = queryWithMutex(id);

        //用逻辑过期解决缓存击穿
        //Shop shop = cacheClient.queryWithLogicalExpire(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, shopId -> getById(shopId), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);

        //返回
        if (shop == null) {
            return Result.fail("缓存空值---店铺不存在！");
        }
        return Result.ok(shop);
    }

    //region 解决缓存击穿(过期方法)
    /*
     //尝试获取锁 成功则返回true
    private boolean tryLock(String key) {
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, 1, RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    //释放锁
    private void freeLock(String key) {
        redisTemplate.delete(key);
    }

    //互斥锁解决缓存击穿的代码 用于public Result queryById(Long id)
    @Deprecated
    private Shop queryWithMutex(Long id) {
        Shop dbShop = null;
        try {
            boolean getKeySuccess;
            while (true) {//获取锁不成功则重复
                //1.根据id从redis查
                Shop shop = (Shop) redisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
                //命中：2.1 若id对应正常数据直接返回
                if (shop != null && shop.getId() != -1L) {
                    return shop;
                }
                //命中：2.2 若id对应空值则返回错误信息
                if (shop != null && shop.getId() == -1L) {
                    return null;
                }
                //未命中：尝试获取锁
                getKeySuccess = tryLock(RedisConstants.LOCK_SHOP_KEY + id);
                if (getKeySuccess) {//获取锁成功，进行后续缓存重建
                    break;
                }
                //获取锁不成功，休眠后重复执行
                Thread.sleep(10);
            }
            //3.根据id查数据库
            dbShop = getById(id);
            //4.数据库中不存在
            if (dbShop == null) {
                //将空值(使用id=-1标识)写入redis
                Shop emptyShop = new Shop();
                emptyShop.setId(-1L);
                redisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, emptyShop, RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
                //返回错误信息
                return null;
            }
            //5.数据库中存在，写入redis，设置缓存过期事件，作为保证数据一致性的兜底方案
            redisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, dbShop, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            //释放锁
            freeLock(RedisConstants.LOCK_SHOP_KEY + id);
        }
        //6.返回
        return dbShop;
    }


    //解决缓存穿透的代码 用于public Result queryById(Long id)
    @Deprecated
    private Shop queryWithPassThrough(Long id) {
        //1.根据id从redis查
        Shop shop = (Shop) redisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
        //2.1 若id对应正常数据直接返回
        if (shop != null && shop.getId() != -1L) {
            return shop;
        }
        //2.2 若id对应空值则返回错误信息
        if (shop != null && shop.getId() == -1L) {
            return null;
        }
        //3.若不存在则查数据库
        Shop dbShop = getById(id);
        //4.数据库中不存在
        if (dbShop == null) {
            //将空值(使用id=-1标识)写入redis
            Shop emptyShop = new Shop();
            emptyShop.setId(-1L);
            redisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, emptyShop, RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
            //返回错误信息
            return null;
        }
        //5.数据库中存在，写入redis，设置缓存过期事件，作为保证数据一致性的兜底方案
        redisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, dbShop, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        //6.返回
        return dbShop;
    }
     */
    //endregion

    /**
     * 根据id更新店铺数据，在更新数据库的同时，删除redis中的数据，保证一致性
     * 增加事务注解保证操作原子性
     * @param shop
     * @return
     */
    @Override
    @Transactional
    public Result updateInfoById(Shop shop) {
        Long id = shop.getId();
        if (id == null) {
            return Result.fail("店铺id不能为空。");
        }
        //更新数据库
        updateById(shop);
        //删除redis中对应的店铺缓存
        redisTemplate.delete(RedisConstants.CACHE_SHOP_KEY + id);
        return Result.ok();
    }

    /**
     * 根据商铺类型分页查询商铺信息
     * @param typeId 商铺类型
     * @param current 页码
     * @param x 店铺坐标 仅根据店铺距离排序时，不为空
     * @param y 店铺坐标
     * @return 商铺列表
     */
    @Override
    public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
        //判断是否需要坐标查询
        if (x == null || y == null) {
            //不需要坐标查询
            // 根据类型分页查询
            Page<Shop> page = query()
                    .eq("type_id", typeId)
                    .page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
            // 返回数据
            return Result.ok(page.getRecords());
        }
        //计算分页参数
        int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
        int end = current * SystemConstants.DEFAULT_PAGE_SIZE;
        //查redis，根据距离分页、排序(member -> shopId)
        String redisKey = RedisConstants.SHOP_GEO_KEY + typeId;
        //查询结果带距离，范围以用位置(x,y)为圆心，半径5km以内，限制查询结果end条
        GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo()
                .radius(redisKey,
                        new Circle(new Point(x, y), new Distance(5000)),
                        RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().limit(end));
        //将结果解析为集合
        if (results == null) {
            return Result.ok(new ArrayList<Shop>());
        }
        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> content = results.getContent();
        //手动截取from到end的结果
        if (from >= content.size()) {
            return Result.ok(new ArrayList<Shop>());
        }
        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> pageContent = null;
        try {
            pageContent = content.stream().skip(from).collect(Collectors.toList());
        } catch (Exception e) {
            log.info("content元素数：{}", content.size());
            log.info("分页错误，此时from = {}, end = {}", from, end);
        }
        //封装返回结果
        List<Shop> shopList = new ArrayList<>();
        for (GeoResult<RedisGeoCommands.GeoLocation<String>> result : pageContent) {
            //解析shopId和对应的距离
            long shopId = Long.parseLong(result.getContent().getName());
            Distance distance = result.getDistance();
            //根据shopId查询shop
            Shop shop = query().eq("id", shopId).one();
            //封装距离信息
            shop.setDistance(distance.getValue());
            shopList.add(shop);
        }
        return Result.ok(shopList);
    }
    /**
     * 新增店铺信息到数据库，同时新增店铺id到布隆过滤器
     * @param shop
     */
    @Override
    public void saveDatabaseAndBloom(Shop shop) {
        save(shop);//店铺信息保存到数据库
        shopIdFilter.add(shop.getId());//将店铺id加入布隆过滤器
    }
}
